package engine.gui;

import static org.lwjgl.opengl.GL11.*;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.FloatBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL12;

import engine.util.draw.Draw2D;

public class BitmapFont implements engine.gui.Font{
	private static final int BASE_CHARACTER = 32;
	private static final int MAX_CHARACTER = 256;
	
	private String fontName;
	private int fontSize;
	
	private int[] widths;
	private int maxCharWidth;
	private int height;
	
	private int ascend;
	private int descend;
	
	private int textureID;
	private float texCoords[][];
	private float texWidth;
	private float texHeight;
	
	public BitmapFont(String fontName, int fontSize, boolean external) {
		this.fontName = fontName;
		this.fontSize = fontSize;
		
		Font font = getFont(external);
		makeBitmap(font);
	}
	
	private Font getFont(boolean external) {
		Font font = null;

		if (external) {
			try {
				InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(fontName);
				font = Font.createFont(Font.TRUETYPE_FONT, inputStream)
						.deriveFont((float)fontSize);
			} catch (FontFormatException fontFormatException) {
				fontFormatException.printStackTrace();
			} catch (IOException e) {
				try {
					InputStream inputStream = new FileInputStream(new File(fontName));
					
					font = Font.createFont(Font.TRUETYPE_FONT, inputStream)
							.deriveFont((float)fontSize);
				} catch (FontFormatException fontFormatException2) {
					fontFormatException2.printStackTrace();
				} catch (FileNotFoundException fileNotFoundException) {
					
					System.out.println(fontName + " could not be found! Fall back to serif/20/plain font. ");
					
					font = new Font("serif", Font.PLAIN, 20);
				} catch (IOException ioException) {
					ioException.printStackTrace();
				} 
			}
		} else {
			font = new Font(fontName, Font.PLAIN, fontSize);
		}
		
		return font;
	}
	
	private void makeBitmap(Font font) {
		//create font metrics corresponding to the current font
		FontMetrics fontMetrics = getFontMetrics(font);
		
		//get width, maxwidth and height
		widths = fontMetrics.getWidths();
		maxCharWidth = fontMetrics.getMaxAdvance();
		height = fontMetrics.getHeight();
		
		//get descend and ascend
		ascend = fontMetrics.getMaxAscent();
		descend = fontMetrics.getMaxDescent();
		
		//compute the numbers of rows and columns of the bitmap
		int numChars = MAX_CHARACTER - BASE_CHARACTER;
		int numRows = (int)Math.sqrt(numChars);
		int numColumns = (int)Math.ceil((float)numChars / numRows);
		
		//compute the image size
		int imageWidth = getNextPowerOfTwo(numColumns * maxCharWidth);
		int imageHeight = getNextPowerOfTwo(numRows * height);
		
		//create space for texture coordinates
		texCoords = new float[numChars][2];
		texWidth = (float)maxCharWidth / imageWidth;
		texHeight = (float)height / imageHeight;
		
		//reserve a buffer for the image
		FloatBuffer floatBuffer = BufferUtils.createFloatBuffer(imageWidth * imageHeight * 4);
		
		//create graphics and buffered image to paint on
		BufferedImage paintImage = new BufferedImage(maxCharWidth, height, BufferedImage.TYPE_4BYTE_ABGR);
		Graphics2D g = paintImage.createGraphics();
		g.setBackground(new Color(0, 0, 0, 0));
		g.setColor(new Color(1, 1, 1, 1));
		g.setFont(font);
	
		//draw any character on the texture
		for(int i = BASE_CHARACTER; i < MAX_CHARACTER; i++) {
			g.clearRect(0, 0, paintImage.getWidth(), paintImage.getHeight());
			
			insertChar(i, numColumns, imageWidth, floatBuffer, paintImage, g);
			makeTextureCoordinates(i, numColumns);
		}
		
		//delete the drawing element of the buffered image
		g.dispose();
		
		//make the texture
		makeTexture(floatBuffer, imageWidth, imageHeight);
	}
	
	private void insertChar(int character, int numColumns, int imageWidth, FloatBuffer buffer, BufferedImage paintImage, Graphics2D g) {
		g.drawString((char)character + "", 0, ascend);
		
		character -= BASE_CHARACTER;
		
		int baseX = (character % numColumns) * paintImage.getWidth();
		int baseY = (character / numColumns) * paintImage.getHeight();
		
		for(int x = 0; x < paintImage.getWidth(); x++) {
			for(int y = 0; y < paintImage.getHeight(); y++) {
				if(paintImage.getRGB(x, y) != 0) {
					int base = ((baseY + y) * imageWidth + (baseX + x)) * 4;
					buffer.put(base, 1);
					buffer.put(base + 1, 1);
					buffer.put(base + 2, 1);
					buffer.put(base + 3, 1);
				}
			}
		}
	}
	
	private void makeTextureCoordinates(int character, int numColumns) {
		character -= BASE_CHARACTER;
		
		int col = character % numColumns;
		int row = character / numColumns;
		
		texCoords[character][0] = col * texWidth;
		texCoords[character][1] = (row - 1) * texHeight;
	}
	
	private FontMetrics getFontMetrics(Font font) {
		BufferedImage buf = new BufferedImage(1, 1,
				BufferedImage.TYPE_BYTE_GRAY);
		Graphics g = buf.getGraphics();
		return g.getFontMetrics(font);
	}
	
	private int getNextPowerOfTwo(int number) {
		int power = 1;
		
		while(power <= number) {
			power *= 2;
		}
		
		return power;
	}
	
	private void makeTexture(FloatBuffer buffer, int imageWidth, int imageHeight) {
		textureID = glGenTextures();
		glBindTexture(GL_TEXTURE_2D, textureID);
		
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		
		buffer.rewind();
		
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imageWidth, imageHeight, 0,
				GL_RGBA, GL_FLOAT, buffer);
		
		glBindTexture(GL_TEXTURE_2D, 0);
	}
	
	@Override
	public void draw(String text, float posX, float posY) {
		if(text == null) {
			return;
		}
		
		glBindTexture(GL_TEXTURE_2D, textureID);
		
		for(int i = 0; i < text.length(); i++) {
			int character = text.charAt(i);
			
			Draw2D.drawImage(posX, posY - height, maxCharWidth, height, texCoords[character][0], texCoords[character][1], texWidth, -texHeight);
			posX += widths[character];
		}
		
		glBindTexture(GL_TEXTURE_2D, 0);
	}

	@Override
	public float getAscend() {
		return ascend;
	}

	@Override
	public float getDescend() {
		return descend;
	}

	@Override
	public float getWidth(String text) {
		if(text == null) {
			return 0;
		}
		
		int length = 0;
		
		for(int i = 0; i < text.length(); i++) {
			length += widths[text.charAt(i)];
		}
		
		return length;
	}
	
	@Override
	public float getHeight() {
		return height;
	}
}
